/*
 * File: Breakout.java
 * -------------------
 * Name: Diego A. Falciola
 * Section Leader: Sam King
 * 
 * Extended implementation of the breakout game
 * 
 * Extensions (five more):
 * 
 * - Score
 * - Ball angle reverted depending on location of colission with paddle
 * - Increase ball speed after INCREASE_SPEED_AFTER_HITS of bricks broken
 * - Added three sounds (game won, game lost, bounce)
 * - Two balls! A second ball will randonmly appear on screen
 * - PLAY FOREVER! You can play again after losing all lives!! 
 * 
 * Possible improvements:
 * - Handle of two balls is done manually. Figure out a way 
 *   of doing it for N balls 
 */

import acm.graphics.*;
import acm.program.*;
import acm.util.*;

import java.applet.AudioClip;
import java.awt.*;
import java.awt.event.*;

public class BreakoutExtended extends GraphicsProgram {

	/** Width and height of application window in pixels */
	public static final int APPLICATION_WIDTH = 400;
	public static final int APPLICATION_HEIGHT = 600;

	/** Dimensions of game board (usually the same) */
	private static final int WIDTH = APPLICATION_WIDTH;
	private static final int HEIGHT = APPLICATION_HEIGHT;

	/** Dimensions of the paddle */
	private static final int PADDLE_WIDTH = 60;
	private static final int PADDLE_HEIGHT = 10;

	/** Offset of the paddle up from the bottom */
	private static final int PADDLE_Y_OFFSET = 30;

	/** Number of bricks per row */
	private static final int NBRICKS_PER_ROW = 10;

	/** Number of rows of bricks */
	private static final int NBRICK_ROWS = 10;

	/** Separation between bricks */
	private static final int BRICK_SEP = 4;

	/** Width of a brick */
	private static final int BRICK_WIDTH =
	  (WIDTH - (NBRICKS_PER_ROW - 1) * BRICK_SEP) / NBRICKS_PER_ROW;

	/** Height of a brick */
	private static final int BRICK_HEIGHT = 8;

	/** Radius of the ball in pixels */
	private static final int BALL_RADIUS = 10;

	/** Offset of the top brick row from the top */
	private static final int BRICK_Y_OFFSET = 70;

	/** Number of turns */
	private static final int NTURNS = 3;

	/** Ball speed */ 
	private static final double BALL_SPEED = 8.0; 

	private static final int PAUSE = 30;

	private static int SCORE_PER_BRICK = 1000;
	
	private static final boolean LIFE_LOST = false;
	
	private static final int INCREASE_SPEED_AFTER_HITS=3;

	// User Messages
	private static final String CLICK_TO_START = "Click to go...";
	private static final String GAME_COMPLETED_WIN 
		= "WooHoo!! no more bricks left! Wanna win again?";
	private static final String GAME_COMPLETED_LOSS 
		= "So sad :( No more lives left. Click and go for the happy ending.";
	private static final String SCORE_MESSAGE = "Score: ";
	private static final String BALL_SPEED_MESSAGE = "Ball Speed: ";
	private static final String LIVES_LEFT_MESSAGE = "Lives left: ";
	
	// game becomes unplayable after certain speed
	private static final int MAX_BALL_SPEED = 50;
	private static final int MIN_PAUSE_TIME = 20;
	
	// the paddle
	private GRect paddle;
	// the ball
	private GOval[] balls = new GOval[2]; 
	// the balls speed
	private double vx, vy, vx2, vy2;
	// bricks left in screen
	int score=0, bricksLeft = NBRICK_ROWS * NBRICKS_PER_ROW;
	// messages displayed to the user
	GLabel messageToUser = new GLabel(""),
		   scoreMessage = new GLabel(""),
		   ballSpeedMessage = new GLabel(""),
		   livesLeftMessage = new GLabel("");
	
	// sounds
	AudioClip gameWonSound = MediaTools.loadAudioClip("woohoo.au");
	AudioClip gameLostSound = MediaTools.loadAudioClip("doh.au");
	AudioClip bounceSound = MediaTools.loadAudioClip("bounce.au");
	
	
	// Now we need to keep track of the bricks
	// So we can remove them and readd them after each play
	GRect[] brickCollection = new GRect[100];
	
	// track collisions in order to increase speed when 
	// INCREASE_SPEED_AFTER_HITS is reached
	int collisions = 0;
	
	/** Sets up the Breakout program. */
	public void init() {		
		addMouseListeners();
	}

	/* Method: run() */
	/** Runs the Breakout program. */
	public void run() {
		// run the game forever :) 
		while(true){
			startGame();
		}
	}
	
	/**
	 *  Core of the game. Initializes elements 
	 *  and lets the ball run
	 */
	private void startGame(){
		int turnsLeft = NTURNS;
		int ballSpeed=PAUSE;
		
		initializeBricks(initializeColors());
		
		// wait for initial click
		waitForStartClick();
		
		initializePaddle();
		
		initializeBall();
		
		initializeLabels();
		
		// continue playing
		// while we have bricks ...
		// ... and lives! 
		while(bricksLeft > 0 && turnsLeft > 0){ 
			// not possible means that a life is lost
			// by hitting bottom wall
			if (!moveBallIfPossible()){
				// substract a life
				turnsLeft =  updateLivesLeft(turnsLeft);
				
				// game becomes unplayable after certain speed
				int currentBallSpeed = getCurrentBallSpeed(ballSpeed);
				
				if (currentBallSpeed>MAX_BALL_SPEED) {
					ballSpeed=MIN_PAUSE_TIME;
				}
				
				// and continue if possible
				if (turnsLeft>0){
					startNewTurn();
				}
			}
			
			// if a brick is hit, revert VY
			checkForCollisionAndRevertMovement();
			
			ballSpeed = handleBallSpeed(collisions, ballSpeed);
			
			// and end when there are no more bricks, YAY! 
			if (bricksLeft==0){
				endGame(GAME_COMPLETED_WIN);
				gameWonSound.play();
				waitForClick();
				return;
			}
			
			pause(ballSpeed);
		}
		
		// no more lives left :(
		endGame(GAME_COMPLETED_LOSS);
		gameLostSound.play();
		waitForClick();
	}
	
	/**
	 *  Displays initial message to the user and waits for user click
	 */
	private void waitForStartClick(){
		drawCenteredText(CLICK_TO_START);
		waitForClick();
		hideCenteredText();
	}
		
	/**
	 *  Starts a new life with a new VX direction
	 */
	private void startNewTurn(){
		for(GOval ball : balls){
			if(ball!=null){
				remove(ball);
			}
		}
		
		remove(paddle);
		
		// calculate new vx, randomly
		randomizeVx();
		
		waitForStartClick();
		hideCenteredText();
		
		initializeBall();
		initializePaddle();
	}
	
	/**
	 * Moves the balls (one or two)
	 * @return true if the ball did not hit the bottom wall
	 */
	private boolean moveBallIfPossible(){		
		for(GOval ball : balls){
			if (ball!=null){
				double ballX = ball.getX();
				double ballY = ball.getY();
				
				double newVx, newVy;
				if(ball==balls[0]){
					newVx=vx;
					newVy=vy;
				}
				else{
					newVx=vx2;
					newVy=vy2;
				}
				
				// check for colission with left and right wall
				if (ballX <= 0 || ballX >= WIDTH-2*BALL_RADIUS) {
					newVx*=-1;
				}
				
				// check for colission with top wall
				if (ballY <= 0){
					newVy *= -1;
				}
				 
				// if bottom wall is hit, lose a life
				if(ballY >= HEIGHT-2*BALL_RADIUS){
					if (ball==balls[0]){
						//newVy *= -1;
						return LIFE_LOST;
					}
					else{
//						newVy *= -1;
						balls[1]=null;
					}
				}
				
				if(ball==balls[0]){
					vx=newVx;
					vy=newVy;
				}else{
					vx2=newVx;
					vy2=newVy;
				}
					
				// move the ball
				ball.move(newVx, newVy);
			}
		}
		
		return true;
	}
	
	/**
	 * Remove paddle and balls from screen
	 * and display message to user
	 * @param endReason message to the user
	 */
	private void endGame(String endReason){
		for(GOval ball : balls){
			if(ball!=null){
				remove(ball);
			}
		}
		remove(paddle);
		score = 0;
		
		drawCenteredText(endReason);
	}
	
	/**
	 *  Sets a random number to VX 
	 */
	private void randomizeVx(){
		RandomGenerator rgen = RandomGenerator.getInstance();
		vx = rgen.nextDouble(1.0, 3.0);
		if (rgen.nextBoolean(0.5)) {
			vx = -vx;
		}
	}
	
	/**
	 * Checks for colission and revert VY movement
	 */
	private int checkForCollisionAndRevertMovement(){
		GPoint[] ballCorners = new GPoint[4];
		
		for(GOval ball : balls){
			if (ball!=null){
				double ballX = ball.getX();
				double ballY = ball.getY();
				
				// setup four corners
				ballCorners[0] = new GPoint(ballX, ballY);
				ballCorners[1] = new GPoint(ballX + BALL_RADIUS, ballY);
				ballCorners[2] = new GPoint(ballX, ballY + BALL_RADIUS);
				ballCorners[3] = new GPoint(ballX + BALL_RADIUS, ballY + BALL_RADIUS);
				
				GObject collidingObject;
				
				// check all four corners for colissions
				for (int ballCornerIndex =0; ballCornerIndex<4; ballCornerIndex++){
					collidingObject = getElementAt(ballCorners[ballCornerIndex].getX(),
										ballCorners[ballCornerIndex].getY());
					
					if (collidingObject != null){
						if (collidingObject!=paddle && (collidingObject instanceof GRect)){
							remove(collidingObject);
							bricksLeft--;
							
							createRandomBall();
							
							collisions++;
							// revert VY direction
							if (ball==balls[0]){
								vy*=-1;
							}
							else{
								vy2*=-1;
							}
							
								
							bounceSound.play();
							
							updateScore(collidingObject.getY());
							break;
						}
						else if(collidingObject==paddle){
							// revert VY direction
							changeVxAccordingToCollisionPosition();

							if (ball==balls[0]){
								vy*=-1;
							}
							else{
								vy2*=-1;
							}
							
							bounceSound.play();
							break;
						}				
					}
				}
			}
		}
		
		
		return collisions;
	}
	
	/**
	 *  Creates a second ball after a couple of bricks broken
	 */
	private void createRandomBall(){
		
		if (bricksLeft%5==0){
			if (balls[1]==null){
				int x = WIDTH / 2;
				int y = HEIGHT / 2;

				GOval ball = new GOval(x, y, BALL_RADIUS, BALL_RADIUS);
				ball.setFillColor(Color.BLACK);
				ball.setFilled(true);
				add(ball);
				
				balls[1] = ball;
				
				RandomGenerator rgen = RandomGenerator.getInstance();
				vx2 = rgen.nextDouble(1.0, 3.0);
				if (rgen.nextBoolean(0.5)) vx = -vx;
				
				vy2= BALL_SPEED;
			}
		}
		
	}
	
	/**
	 *  changes Vx according to the side where the ball collided with 
	 *  the paddle
	 */
	private void changeVxAccordingToCollisionPosition(){
		for(GOval ball : balls){
			double newVx =0;
			
			if (ball!=null){
				double dif = ball.getX() - paddle.getX();
				
				RandomGenerator rgen = RandomGenerator.getInstance();
				
				if (dif<10){
					newVx = rgen.nextDouble(1.0, 3.0) * -1;
				}else if(dif >= 10 && dif <= 40){
					newVx = rgen.nextDouble(1.0, 1.5) ;
				}else{
					newVx = rgen.nextDouble(1.5, 3.5);
				}
				
				if (ball==balls[0]){
					vx=newVx;
				}
				else{
					vx2=newVx;
				}
			}
		}
	}
	
	private int getCurrentBallSpeed(int ballSpeedPauseTime){
		return (int)(BALL_SPEED/ballSpeedPauseTime*100);
	}
	
	/**
	 * increase speed if collisions reached INCREASE_SPEED_AFTER_HITS
	 * @param collisions counter of previous collisions 
	 * since last speed increase
	 * @param ballSpeedPauseTime time to pause to simulate speed
	 * @return the new ball speed
	 */
	private int handleBallSpeed(int collisions, int ballSpeedPauseTime){
		if (collisions==INCREASE_SPEED_AFTER_HITS){
			ballSpeedPauseTime--;
			
			int ballSpeedOverPause = getCurrentBallSpeed(ballSpeedPauseTime) ;
			ballSpeedMessage.setLabel(BALL_SPEED_MESSAGE + ballSpeedOverPause);
			
			if (ballSpeedPauseTime<1){
				ballSpeedPauseTime=1;
			}
			
			this.collisions=0;
		}
		
		return ballSpeedPauseTime;
	}
	
	/**
	 * Updates score according to brick location
	 * Lower rows -> lower score
	 * Upper rows -> higher score 
	 * @param brickLocation
	 */
	private void updateScore(double brickLocation){
		// points per brick increases per row 
		double brickRow = brickLocation/BRICK_HEIGHT;
		score += SCORE_PER_BRICK / brickRow;
		scoreMessage.setLabel(SCORE_MESSAGE + String.valueOf(score));
	}
	
	/**
	 * Updates the number of turns left for the user to play
	 * @param turns current turns left
	 * @return new turns left
	 */
	private int updateLivesLeft(int turns){
		turns--;
		livesLeftMessage.setLabel(LIVES_LEFT_MESSAGE + turns);
		return turns;
	}
  	
	/**
	 *  Setup a list of colors for bricks
	 *  @return a list of brick colors by row
	 */
	private Color[] initializeColors(){
		Color[] colors = new Color[NBRICK_ROWS];
		
		colors[0] = colors[1] = Color.RED;
		colors[2] = colors[3] = Color.ORANGE;
		colors[4] = colors[5] = Color.YELLOW;
		colors[6] = colors[7] = Color.GREEN;
		colors[8] = colors[9] = Color.CYAN;
		
		return colors;
	}
	
	/**
	 *  Creates a new paddle and draws it in screen
	 */
	private void initializePaddle(){
		if(paddle==null){
			GPoint point = getPaddleInitialPoint();
			
			paddle = new GRect(point.getX(), point.getY(), PADDLE_WIDTH, PADDLE_HEIGHT);
			paddle.setFilled(true);
		}
		
		add(paddle);
	}
	
	/**
	 *  Creates a new ball, sets its VX and VY and draws it in screen
	 */
	private void initializeBall(){
		int x = WIDTH / 2;
		int y = HEIGHT / 2;
		
		balls[0] = balls[1] = null;

		GOval ball = new GOval(x, y, BALL_RADIUS, BALL_RADIUS);
		ball.setFillColor(Color.BLACK);
		ball.setFilled(true);
			
		balls[0] = ball;
		
		add(balls[0]);
		
		randomizeVx();
		
		vy = BALL_SPEED;
	}
	
	/**
	 *  Remove all existing bricks and draw NBRICK_ROWS rows in screen
	 */
	private void initializeBricks(Color[] brickColors){
		// check if a brick exists
		// then assume all bricks were loaded
		if (brickCollection[0]!=null){
			for(int brick=0; brick<brickCollection.length; brick++){
				if(brickCollection[brick]!=null){
					remove(brickCollection[brick]);
					brickCollection[brick]=null;
				}
			}
		}
		
		for (int row = 0; row < NBRICK_ROWS; row++){
			drawLineOfBricks(row, brickColors);
		}
		
		bricksLeft = NBRICK_ROWS * NBRICKS_PER_ROW;
	}
	
	/**
	 *  Initialize the labels that are displayed to the user
	 */
	private void initializeLabels(){
		scoreMessage.setLabel(SCORE_MESSAGE);
		scoreMessage.setLocation(10,20);
		add(scoreMessage);
		
		livesLeftMessage.setLabel(LIVES_LEFT_MESSAGE + NTURNS);
		livesLeftMessage.setLocation(getWidth()/2-50, 20);
		add(livesLeftMessage);

		int ballSpeedOverPause = (int) (BALL_SPEED/PAUSE*100);
		ballSpeedMessage.setLabel(BALL_SPEED_MESSAGE + ballSpeedOverPause);
		ballSpeedMessage.setLocation(getWidth()-100, 20);
		add(ballSpeedMessage);
	}
	
	/**
	 * Draw a line of bricks in screen
	 * @param row row number
	 * @param brickColors a list of possible brick colors
	 */
	private void drawLineOfBricks(int row, Color[] brickColors){
		for (int brickNumber = 0; brickNumber<NBRICKS_PER_ROW; brickNumber++){
			GPoint brickInitialPoint = getBrickInitialPoint(brickNumber, row);
			
			GRect rect = new GRect(brickInitialPoint.getX()
					, brickInitialPoint.getY()
					, BRICK_WIDTH, BRICK_HEIGHT);
			rect.setFilled(true);
			rect.setColor(brickColors[row]);
			add(rect);
			
			brickCollection[brickNumber + 10*row] = rect;
		}
	}
	
	/**
	 * Calculate where a brick is displayed
	 * depending on its column and row
	 * 
	 * @param column column number
	 * @param row row number
	 * @return a GPoint object with brick X and Y position
	 */
	private GPoint getBrickInitialPoint(int column, int row){
		GPoint point = new GPoint();
		
		int x =  column * BRICK_WIDTH+ (column+1)*BRICK_SEP;
		int y =  row * BRICK_HEIGHT + row*BRICK_SEP + BRICK_Y_OFFSET;
		
		point.setLocation(x,y);
		
		return point;
	}
	
	/**
	 * Calculate X and Y position of the Paddle
	 * @return a GPoint object with paddle X and Y position
	 */
	private GPoint getPaddleInitialPoint(){
		int x = getWidth()/2 - PADDLE_WIDTH/2;
		int y = getHeight() - PADDLE_Y_OFFSET;
		
		return new GPoint(x,  y);
	}
	
	
	/**
	 *   Mouse move event
	 */ 
	public void mouseMoved(MouseEvent e) {
		
		if (paddle!=null){
			// x and y location of mouse 
			Point newPosition = e.getPoint();
			
			if (newPosition.x < ((WIDTH- PADDLE_WIDTH)+ PADDLE_WIDTH/2) ){
				double newPositionX = newPosition.x - PADDLE_WIDTH/2;
				
				// do not let the paddle go off the screen
				if (newPositionX<0) newPositionX = 0;
				
				paddle.setLocation(newPositionX,
						HEIGHT - (PADDLE_Y_OFFSET + PADDLE_HEIGHT));
			}
		}
		
	}
	
	/**
	 * Draw a GLabel centered on screen
	 * @param text GLabel text
	 */
	private void drawCenteredText(String text){
		messageToUser.setLabel(text);
		messageToUser.setColor(Color.RED);
		messageToUser.setLocation(getWidth()/2-(text.length()/2*5.5)
				, getHeight()/2);
		add(messageToUser);
	}
	
	/**
	 *  Hides centered text
	 */
	private void hideCenteredText(){
		remove(messageToUser);
	}
}
